diff --git a/web/avatars/avatar.css b/web/avatars/avatar.css index 86ba383a1..27ca9ccbe 100644 --- a/web/avatars/avatar.css +++ b/web/avatars/avatar.css @@ -1,61 +1,72 @@ +.avatarContainer { + position: relative; + display: flex; + justify-content: center; + align-items: center; +} + +.editAvatarLoadingSpinner { + position: absolute; +} + .emojiContainer { display: flex; align-items: center; justify-content: center; } .emojiMicro { font-size: 9px; height: 16px; line-height: 16px; } .emojiSmall { font-size: 14px; height: 24px; line-height: 24px; } .emojiLarge { font-size: 28px; height: 42px; line-height: 42px; } .emojiProfile { font-size: 80px; height: 112px; line-height: 112px; } .micro { border-radius: 8px; height: 16px; width: 16px; min-width: 16px; } .small { border-radius: 12px; height: 24px; width: 24px; min-width: 24px; } .large { border-radius: 21px; height: 42px; width: 42px; min-width: 42px; } .profile { border-radius: 56px; height: 112px; width: 112px; min-width: 112px; } .imgContainer { object-fit: cover; } diff --git a/web/avatars/avatar.react.js b/web/avatars/avatar.react.js index 108f9ade1..507be4ed5 100644 --- a/web/avatars/avatar.react.js +++ b/web/avatars/avatar.react.js @@ -1,69 +1,96 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import type { ResolvedClientAvatar } from 'lib/types/avatar-types.js'; import css from './avatar.css'; +import LoadingIndicator from '../loading-indicator.react.js'; type Props = { +avatarInfo: ResolvedClientAvatar, +size: 'micro' | 'small' | 'large' | 'profile', + +showSpinner?: boolean, }; function Avatar(props: Props): React.Node { - const { avatarInfo, size } = props; + const { avatarInfo, size, showSpinner } = props; const containerSizeClassName = classnames({ [css.imgContainer]: avatarInfo.type === 'image', [css.micro]: size === 'micro', [css.small]: size === 'small', [css.large]: size === 'large', [css.profile]: size === 'profile', }); const emojiSizeClassName = classnames({ [css.emojiContainer]: true, [css.emojiMicro]: size === 'micro', [css.emojiSmall]: size === 'small', [css.emojiLarge]: size === 'large', [css.emojiProfile]: size === 'profile', }); const emojiContainerColorStyle = React.useMemo(() => { if (avatarInfo.type === 'emoji') { return { backgroundColor: `#${avatarInfo.color}` }; } return undefined; }, [avatarInfo.color, avatarInfo.type]); const avatar = React.useMemo(() => { if (avatarInfo.type === 'image') { return ( image avatar ); } return (
{avatarInfo.emoji}
); }, [ avatarInfo.emoji, avatarInfo.type, avatarInfo.uri, containerSizeClassName, emojiContainerColorStyle, emojiSizeClassName, ]); - return avatar; + let loadingIndicatorSize; + if (size === 'micro') { + loadingIndicatorSize = 'small'; + } else if (size === 'small') { + loadingIndicatorSize = 'small'; + } else if (size === 'large') { + loadingIndicatorSize = 'medium'; + } else { + loadingIndicatorSize = 'large'; + } + + const loadingIndicator = React.useMemo( + () => ( +
+ +
+ ), + [loadingIndicatorSize], + ); + + return ( +
+ {showSpinner ? loadingIndicator : null} + {avatar} +
+ ); } export default Avatar; diff --git a/web/avatars/emoji-avatar-selection-modal.react.js b/web/avatars/emoji-avatar-selection-modal.react.js index 68d4292a8..567294c76 100644 --- a/web/avatars/emoji-avatar-selection-modal.react.js +++ b/web/avatars/emoji-avatar-selection-modal.react.js @@ -1,145 +1,149 @@ // @flow import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; import invariant from 'invariant'; import * as React from 'react'; import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { defaultAnonymousUserEmojiAvatar, getAvatarForUser, getDefaultAvatar, } from 'lib/shared/avatar-utils.js'; import type { ClientAvatar, ClientEmojiAvatar, } from 'lib/types/avatar-types.js'; import Avatar from './avatar.react.js'; import css from './emoji-avatar-selection-modal.css'; import Button, { buttonThemes } from '../components/button.react.js'; import LoadingIndicator from '../loading-indicator.react.js'; import Modal from '../modals/modal.react.js'; import ColorSelector from '../modals/threads/color-selector.react.js'; import { useSelector } from '../redux/redux-utils.js'; function EmojiAvatarSelectionModal(): React.Node { const { popModal } = useModalContext(); const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { setUserAvatar, userAvatarSaveInProgress } = editUserAvatarContext; const [updateAvatarStatus, setUpdateAvatarStatus] = React.useState(); const currentUserInfo = useSelector(state => state.currentUserInfo); const currentUserAvatar: ClientAvatar = getAvatarForUser(currentUserInfo); const defaultUserAvatar: ClientEmojiAvatar = currentUserInfo?.username ? getDefaultAvatar(currentUserInfo.username) : defaultAnonymousUserEmojiAvatar; // eslint-disable-next-line no-unused-vars const [pendingAvatarEmoji, setPendingAvatarEmoji] = React.useState( currentUserAvatar.type === 'emoji' ? currentUserAvatar.emoji : defaultUserAvatar.emoji, ); const [pendingAvatarColor, setPendingAvatarColor] = React.useState( currentUserAvatar.type === 'emoji' ? currentUserAvatar.color : defaultUserAvatar.color, ); const pendingEmojiAvatar: ClientEmojiAvatar = React.useMemo( () => ({ type: 'emoji', emoji: pendingAvatarEmoji, color: pendingAvatarColor, }), [pendingAvatarColor, pendingAvatarEmoji], ); const onEmojiSelect = React.useCallback(selection => { setUpdateAvatarStatus(); setPendingAvatarEmoji(selection.native); }, []); const onColorSelection = React.useCallback((hex: string) => { setUpdateAvatarStatus(); setPendingAvatarColor(hex); }, []); const onSaveAvatar = React.useCallback(async () => { try { await setUserAvatar(pendingEmojiAvatar); setUpdateAvatarStatus('success'); } catch { setUpdateAvatarStatus('failure'); } }, [pendingEmojiAvatar, setUserAvatar]); let saveButtonContent; let buttonColor; if (userAvatarSaveInProgress) { buttonColor = buttonThemes.standard; saveButtonContent = ; } else if (updateAvatarStatus === 'success') { buttonColor = buttonThemes.success; saveButtonContent = ( <> {'Avatar update succeeded.'} ); } else if (updateAvatarStatus === 'failure') { buttonColor = buttonThemes.danger; saveButtonContent = ( <> {'Avatar update failed. Please try again.'} ); } else { buttonColor = buttonThemes.standard; saveButtonContent = 'Save Avatar'; } return (
- +
); } export default EmojiAvatarSelectionModal;